$def with (race, url, manager)

<html>
  <head>

  <!-- jQuery -->
    <script src="./static/assets/code.jquery.com/jquery-1.11.2.min.js"></script>
    <script src="./static/assets/code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
    
    <!-- jQuery UI -->
    <link rel="stylesheet" href="./static/assets/code.jquery.com/ui/1.11.2/themes/excite-bike/jquery-ui.css">    
    <script src="./static/assets/code.jquery.com/ui/1.11.2/jquery-ui.min.js"></script>
    
    <style type="text/css">
      table.heat-tbl {
        width: 100%;
        border-collapse: collapse;
      }
      
      td {
        text-align: center;
        border-left: 1px solid #000;
      }

      td:first-child {
        border-left: none;
      }
      
      img.contestant-picture {
        /*width: 240px;*/
        /*height: 320px;*/
      }

      td.heat-td-lane {
        font-size: large;
        font-weight: bold;
        height: 30px;
      }
      
      td.heat-td-name {
        font-size: xx-large;
        font-weight: bold;
        height: 60px;
      }

      td.heat-td-time {
        font-size: xx-large;
        font-weight: bold;
        height: 60px;
      }      

      td.heat-td-rank {
        font-size: xx-large;
        font-weight: bold;
        height: 60px;
      }   
      
      </style>
    
  </head>
  <body>
    <p align="center"><h2>the race is $race</h2></p>
    <div id="manager">
      <table style="cell-padding: 15px;width: 100%">
        <tr>
          <td>
      <h2>View race results on your device at $url</h2>
          </td>
          <td style="border: none; vertical-align: middle;">
          <br/>
      <form action="/runRace" method="POST">
        <input type="hidden" name="race" value="$race">
        <button type="submit" name="action" id="action" value="advance heat">Save Heat</button>
      </form>
          </td>
          <td style="border: none; vertical-align: middle;">
      <button id="reset" >Reset</button>
          </td>
        </tr>
      </table>
    </div>    
    <div id="tabs"></div>   
    <button id="refresh" onclick="window.location.reload(true);" >Refresh</button>
    <script type="text/javascript">
    
      // the interval (in milliseconds) at which this page will check times.json
      var TIMER_INTERVAL = 300; 
    
      // the object loaded from raceData.json
      var raceData = null;
      
      // used to keep track of the Last-Modified date of times/raceData
      var lastModifiedRace = null;
      var lastModifiedTimes = null;
    
      // indicates whether or not current request is a manager or viewer
      var manager = $manager;
    
      var jq = jQuery.noConflict();
    
      jq("document").ready(function(){

        // only show buttons for manager
        if ( manager == false ) {
          jq("#manager").hide();

          // web.py is adding a "return" link at the bottom of the page ... hide it for viewers
          jq("a[href='/chooseRace']").hide();          
        } 
        
        // AJAX call to get race data
        getRaceData();
              
        function getRaceData() {
          //get raceData.json
          jq.ajax({
            dataType: "json",
            url: "static/${race}/raceData.json",
            success: loadRaceData,
            error: function(xhr, textStatus, errmsg) {
              console.error(errmsg);
              setTimeout(getRaceData, 3000);
            }
          });
        }
        
        function loadRaceData(data, textStatus, request) {
          try {
            
            lastModifiedRace = new Date(request.getResponseHeader("Last-Modified"));
            raceData = data;
            
            var tabs = jq("#tabs");
            var ul = jq("<ul/>").appendTo(tabs);
            
            // build tabs for each heat
            jq.each(data.heats, function(i, heat){
              
              // title
              var title = jq("<li/>").appendTo(ul);
              var txt = "Heat " + (i + 1).toString();
              if ( i == (data.heats.length - 1) && data.status == "championship" ) txt = "Championship";
              if ( i == data.currentHeat ) {
                txt = "**" + txt + "**";
              }              
              
              var a = jq("<a/>").attr("href","#tabs-" + i).text(txt).appendTo(title);
              
              // content
              var tab = jq("<div/>").attr("id","tabs-" + i).appendTo(tabs);
              var tbl = jq("<table/>").attr("id","heat-tbl-" + i).addClass("heat-tbl").appendTo(tab);
              var trlane = jq("<tr/>").addClass("heat-tr-lane").appendTo(tbl);
              var trpic = jq("<tr/>").addClass("heat-tr-pic").appendTo(tbl);
              var trname = jq("<tr/>").addClass("heat-tr-name").appendTo(tbl);
              var trtime = jq("<tr/>").addClass("heat-tr-time").appendTo(tbl);
              var trrank = jq("<tr/>").addClass("heat-tr-rank").appendTo(tbl);
              
              jq.each(heat, function(j, lane){
              
                // lane
                var tdlane = jq("<td/>").addClass("heat-td-lane").appendTo(trlane).text("Lane " + (j+1).toString());
              
                // picture
                var tdpic = jq("<td/>").addClass("heat-td-pic").appendTo(trpic);
                var img = jq("<img/>")
                            .addClass("contestant-picture")
                            .appendTo(tdpic);
                if ( lane[0] == "bye" ) {
                  img.attr("src","./static/assets/images/transparent.png")
                    .attr("width","240px").attr("height","320px");
                } else {
                  img.attr("src","./static/${race}/" + lane[0] + ".jpeg");
                }
                            
                // name
                var tdname = jq("<td/>").addClass("heat-td-name").appendTo(trname);
                tdname.data("name",lane[0]);
                if ( lane[0] == "bye" ) {
                  tdname.text("empty lane");
                } else {
                  jq.each(data.contestants, function(n, ident) {
                    if (ident == lane[0]) tdname.text(data.names[n]);
                  });
                }

                // time
                var tdtime = jq("<td/>").addClass("heat-td-time").appendTo(trtime);           
                tdtime.data("microseconds", lane[1]);
                
                // rank
                var tdrank = jq("<td/>").addClass("heat-td-rank").appendTo(trrank);           
                tdrank.text("");                
              });
                            
            });
            
            // convert DIVs to tabs
            var showHeat = data.currentHeat;
            var show = jq.getUrlVar("show");
            if ( show == "next" ) showHeat = showHeat + 1;
            
            if ( showHeat > (data.heats.length - 1)) showHeat = (data.heats.length - 1); 
            jq("#tabs").tabs({ active: showHeat });
            
            // if we already saved the championship round, then hide the buttons
            if ( data.currentHeat >= data.heats.length ) jq("#manager").hide();
            
            // assign ranks for each heat
            jq.each(data.heats, function(i, heat){
              rankHeat("heat-tbl-"+i);
            });
            
            if ( manager ) {
            
              // add the handler for the reset button
              jq("#reset").click(function() {
                resetHeat("heat-tbl-"+data.currentHeat);
              });
            
              // start the timer
              setTimeout(checkTimes, TIMER_INTERVAL);
            }
            
          } catch (ex) {
            console.error(ex.message);
            // if error occurs, try again
            setTimeout(getRaceData, 3000);
          }
        };

        
        // this function will review all the times for the given heat and rank the lanes
        function rankHeat(heatid) {
          try {
            var tbl = jq("#"+heatid);

            var tdnames = tbl.find(".heat-td-name");            
            var tdtimes = tbl.find(".heat-td-time");
            var tdranks = tbl.find(".heat-td-rank");
            
            // get all times into array
            var max = 10000000000;
            var times = [];
            jq.each(tdtimes, function(i, td) {
              var t = jq(td).data("microseconds");
              if ( t === undefined || t == 0 ) times.push(max);
              else times.push(t);
            });
            
            // sort the times numerically
            times.sort(function(a,b){return a-b});
            
            // assign ranks
            for(i = 0; i < times.length; i++){
              if (times[i] < max) {
                for(j = 0; j < tdtimes.length; j++) {
                  var tdn = tdnames.get(j);
                  if ( jq(tdn).data("name") == "bye" ) continue;
                  var tdt = tdtimes.get(j);
                  var tdr = tdranks.get(j);
                  if (jq(tdt).data("microseconds") == times[i]) {
                    var ms = Math.round(times[i] / 1000.0);
                    jq(tdt).text((ms / 1000.0).toFixed(3) + " s");
                    switch(i){
                      case 0:
                        jq(tdr).text("1st");
                        break;
                      case 1:
                        jq(tdr).text("2nd");
                        break;  
                      case 2:
                        jq(tdr).text("3rd");
                        break;
                      case 3:
                        jq(tdr).text("4th");
                        break; 
                      default:
                        break;
                    }
                  }
                }
              }
            }
            
          } catch (ex) {
            console.error(ex.message);
          }
        };
                        
        // this function will reset all the times/ranks for the given heat
        function resetHeat(heatid) {
          try {
            var tbl = jq("#"+heatid);
      
            var tdtimes = tbl.find(".heat-td-time");
            var tdranks = tbl.find(".heat-td-rank");
            
            // reset the timestamp so the timer won't display times until times.json changes again
            lastModifiedRace = lastModifiedTimes;
            
            jq.each(tdtimes, function(i,td) {
              jq(td).data("microseconds",0).text("");
            });
            
            jq.each(tdranks, function(i,td) {
              jq(td).text("");
            });              
            
          } catch (ex) {
            console.error(ex.message);
          }
        };

        function checkTimes() {
          try {
            jq.ajax({
              dataType: "json",
              url: "static/times.json",
              cache: false,
              success: function(times, textStatus, request) {
                try {
                  // check header to see if the file has been updated
                  lastModifiedTimes = new Date(request.getResponseHeader("Last-Modified"));
                  if ( lastModifiedTimes > lastModifiedRace ) {

                    // assign times and rank heat
                    var currentHeat = "heat-tbl-"+raceData.currentHeat;              
                    var tbl = jq("#"+currentHeat);       
                    var tdtimes = tbl.find(".heat-td-time");
                    var tdranks = tbl.find(".heat-td-rank");                    
                    jq.each(tdtimes, function(i, td) {
                      jq(td).data("microseconds", times[i]);
                      if ( times[i] == 0 ) {
                        jq(td).text("");
                        jq(tdranks.get(i)).text("");
                      }
                    });
                    rankHeat(currentHeat);
                    
                    /*
                     * I was setting lastModified here so that the code would not process the same times.json file twice
                     * however, Eric reported scenarios where the times were not being shown correctly (or at all)
                     * as an attempt to resolve that, I am commenting the next line so that any times.json that is 
                     * newever than raceData.json will be processed (even though the same times.json may be processed
                     * multiple times). The issue may occur if times.json is written twice within a second.
                    */
                    //lastModified = modified;
                  }
                  
                  // reset timer
                  if ( manager ) setTimeout(checkTimes, TIMER_INTERVAL);
                } catch (ex) {
                  console.error(ex.message)
                  if ( manager ) setTimeout(checkTimes, TIMER_INTERVAL);
                }
              },
              error: function(xhr, status, msg) {
                console.error(msg);
                if ( manager ) setTimeout(checkTimes, TIMER_INTERVAL);
              }
            });
          } catch (ex) {
            console.error(ex.message);
            if ( manager ) setTimeout(checkTimes, TIMER_INTERVAL);
          }
        };                
        
      });  
    
    jq.extend({
      getUrlVars: function(){
        var vars = [], hash;
        var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
        for(var i = 0; i < hashes.length; i++)
        {
          hash = hashes[i].split('=');
          vars.push(hash[0]);
          vars[hash[0]] = hash[1];
        }
        return vars;
      },
      getUrlVar: function(name){
        return jq.getUrlVars()[name];
      }
    }); 
    
    </script>
  </body>
</html>
